디버거 원리

운영체제는 기본적으로 프로세스를 격리합니다. 하지만

운영체제의 대원칙은 프로세스 격리이며, 각 프로세스 쓰리는 자신만의 독립된 가상 메모리 공간을 가지고 있습니다. 또한 프로세스의 주소 공간을 들여다볼 수 없습니다. 하지만 디버거의 경우 타 프로세스의 메모리를 조회, 변조가 가능합니다.

그 이유는 Kernel에 있습니다. 커널의 경우 프로세스를 격리하는 역할도 하지만 디버깅을 위해 다른 프로세스의 메모리 조회, 변조가 가능하게 합니다. 여기서 프로세스가 다른 프로세스의 메모리에 직접 접근하는 것을 허락하는 것이 아닌, 다른 프로세스의 메모리에 접근을 커널에게 요청하면 대신 수행해준다는 것을 주의해야 합니다.

하드웨어 중단점 & 소프트웨어 중단점

1. 소프트웨어 중단점: 명령어 가로채기 (Trap)

  • 프로세스의 메모리 내용을 직접 수정한다.
  1. 명령어 치환 : 중단점을 설정하려는 위치의 기존 명령어를 다른 곳에 잠시 저장한 후 CPU가 읽을 때 예외를 발생시키는 특수 명령어로 덮음 x86 : INT 3, ARM : BRK
  2. 예외 발생과 핸들링 : CPU가 특수 명령어를 실행하는 순간, 예외 발생을 알리며 커널의 예외 처리 루틴을 호출
  3. 제어권 획득 : OS는 예외가 디버깅 중인 프로세스에서 발생했음을 감지 CPU 상태와 메모리 상태를 디버거에게 알려줌.
  4. 실행 재개 : 디버깅 완료 후 예외 발생 특수 명령어로 복원 후 OS에게 실행을 이어나가라고 신호 전달.

2. 하드웨어 중단점: CPU의 감시 모드

  • 코드 수정 없이 CPU에 내장된 디버거 전용 레지스터를 이용하는 방식.
  1. 감시 주소 등록 : 디버거가 CPU 내부 특수 목적 레지스터에 특정 주소를 감시하라고 명령
  2. 하드웨어 레벨의 비교 : CPU는 매 명령을 수행할 때마다 현재 처리 중인 주소 및 레지스터에 등록된 감시 주소를 비교
  3. 비교 일치 시 정지 : 주소가 일치할 경우 CPU는 즉시 실행을 중단하고 OS를 통해 디버거를 호출

안티 디버깅이란?

자신의 소프트웨어를 디버깅하는 것을 막기 위해 디버그 프로그램을 감지하고, 감지될 경우 실행이 정상적으로 되지 않도록 막는 기법

안티 디버깅의 구현 및 우회 방법

디버깅 탐지 방어 코드

int main() {
	if (detect_debugger()) {
		garbage_code();
		exit(-1);
	}
	
	my_secret_code();
	
	return 0;
}

위 코드의 경우 my_secret_code()라는 함수를 보호하기 위해서 해당 함수가 실행되기 전에 detect_debugger()라는 함수를 통해서 디버거를 탐지하고, true일 경우 종료하는 코드입니다.

우회 방법 1. 코드 패치

int main() {
	if (false) {
		garbage_code();
		exit(-1);
	}
	
	my_secret_code();
	
	return 0;
}

조건문 부분에 detect_debugger()라는 코드를 강제적으로 false라는 코드로 바꿔버려서 안티디버거가 동작하지 않도록 만들어 버릴 수 있습니다.

안티 디버깅 기술 분류

  • Artifact-based : 프로세스 내부에 남기는 흔적을 찾아내는 방법
  • Behavior-based : 디버깅 프로그램 실행 중 발생하는 디버깅 특유의 동작을 감지하는 방법
  • Debugging Prevention : 디버깅 시도가 완전 불가능하도록 원천적으로 차단하는 방법

Artifact-Based 안티 디버깅

Artifact-Based는 디버거가 실행되는 동안 시스템의 메모리, 파일 시스템 등의 고유한 흔적을 남길을 통해서 탐지하여 디버깅 중이라고 간주한다.

WinDbg 안티 디버깅 실습

해당 코드는 대표적인 디버거명을 프로세스에 실행하는 파일명과 비교하는 방식으로 동작하며, DbgX.shell.exe는 WinDbg를 최신 WinDBG Preview 버전으로 설치할 경우의 프로세스 명입니다.

#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include <string>
 
// List of known debugger process names
std::vector<std::wstring> debuggerNames = {
    L"x64dbg.exe",
    L"x32dbg.exe",
    L"DbgX.Shell.exe",
    L"ida.exe",
    L"ida64.exe"
};
 
// Check running processes to see if any known debugger is active
bool IsDebuggerRunning() {
    bool detected = false;
    HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
 
    // Take a snapshot of all running processes
    if (hSnapShot == INVALID_HANDLE_VALUE) {
        return false;
    }
 
    PROCESSENTRY32W pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32W);
 
    // Iterate through all processes in the snapshot
    if (Process32FirstW(hSnapShot, &pe32)) {
        do {
            // Compare process name with known debugger names
            for (const auto& debugName : debuggerNames) {
                if (_wcsicmp(pe32.szExeFile, debugName.c_str()) == 0) {
                    std::wcout << L"[!] Debugger Detected: " << pe32.szExeFile << std::endl;
                    detected = true;
                    break;
                }
            }
            if (detected) break;
        } while (Process32NextW(hSnapShot, &pe32));
    }
 
    // Release snapshot handle
    CloseHandle(hSnapShot);
    return detected;
}
 
int main() {
    std::cout << "--- Artifact-based Check: Process Name ---" << std::endl;
 
    // Run debugger detection check
    if (IsDebuggerRunning()) {
        std::cout << "Result: Debugger Detected!" << std::endl;
    }
    else {
        std::cout << "Result: No Debugger Found." << std::endl;
    }
 
    return 0;
}

WinDbg를 켜 둔 상태로 위 코드를 빌드하고 실행할 경우WinDbg를 탐지함을 확인할 수 있습니다.

--- Artifact-based Check: Process Name ---
[!] Debugger Detected: DbgX.Shell.exe
Result: Debugger Detected!

위 경우 디버깅 프로세스 이름을 다른 것으로 변경하는 방식으로 쉽게 우회할 수 있다.

Note

현대에는 디버거 프로세스의 이름으로 탐지하지 않고, 디버거의 프로세스에서 해시 등의 시그니처 값들을 추출한 뒤 시그니처가 일치하는지 비교하여 디버거를 탐지하도록 발전하였다.

Window PEB(Process Environment Block) 구조체

PEB는 커널에서 하나씩 생성하는 프로세스 관리 목적의 내부적인 기록하는 시스템 메타데이터이다. 메타데이터는 OS가 디버깅을 위해 기록해두는 여러 필드가 존재하며, 데이터를 이용해 프로세스가 디버깅되고 있는지 여부를 확인할 수 있다.

중요 필드 값